/*
 * MegaMek - Copyright (C) 2003,2004,2005 Ben Mazur (bmazur@sev.org)
 *
 *  This program is free software; you can redistribute it and/or modify it
 *  under the terms of the GNU General Public License as published by the Free
 *  Software Foundation; either version 2 of the License, or (at your option)
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful, but
 *  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 *  or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 *  for more details.
 */

package megamek.client.bot;

import java.util.Enumeration;
import java.util.Iterator;

import megamek.common.BattleArmor;
import megamek.common.Compute;
import megamek.common.Coords;
import megamek.common.Entity;
import megamek.common.IGame;
import megamek.common.IHex;
import megamek.common.INarcPod;
import megamek.common.Infantry;
import megamek.common.Mech;
import megamek.common.Mounted;
import megamek.common.Protomech;
import megamek.common.Tank;
import megamek.common.TargetRoll;
import megamek.common.Terrains;
import megamek.common.ToHitData;
import megamek.common.actions.BrushOffAttackAction;
import megamek.common.actions.ClubAttackAction;
import megamek.common.actions.KickAttackAction;
import megamek.common.actions.PunchAttackAction;
import megamek.common.actions.PushAttackAction;

// TODO: Auto-generated Javadoc
/**
 * The Class PhysicalCalculator.
 */
public final class PhysicalCalculator {

	private static final double MAX_VALUE = 100.0;
	private static final double MINIMAL_BREACH = 1.5;

	/**
	 * Calculate physical turn.
	 * 
	 * @param bot
	 *            the bot
	 * @return the physical option
	 */
	static PhysicalOption calculatePhysicalTurn(final TestBot bot) {
		int entNum = bot.game.getFirstEntityNum(bot.getMyTurn());
		final int first = entNum;
		Entity en;
		PhysicalOption bestAttack;
		do {
			// take the first entity that can do an attack
			en = bot.game.getEntity(entNum);
			bestAttack = getBestPhysical(en, bot.game);

			if (bestAttack != null) {

				return bestAttack;

			} // End no-attack
			entNum = bot.game.getNextEntityNum(entNum);

		} while ((entNum != -1) && (entNum != first));

		// Didn't find any physical attack.
		return null;
	}

	/**
	 * Gets the best physical.
	 * 
	 * @param entity
	 *            the entity
	 * @param game
	 *            the game
	 * @return the best physical
	 */
	static PhysicalOption getBestPhysical(final Entity entity, final IGame game) {
		// Infantry can't conduct physical attacks.
		if (entity instanceof Infantry) {
			return null;
		}

		// if you're charging, it's already declared
		if (entity.isCharging() || entity.isMakingDfa()) {
			return null;
		}

		PhysicalOption best = null;
		ToHitData odds;
		double breach;
		double breachA;
		double lDmg;
		double rDmg;
		double finalDmg;
		int bestBrush = PhysicalOption.NONE;

		// If the attacker is a Mech

		if (entity instanceof Mech) {

			lDmg = 0.0;
			rDmg = 0.0;
			finalDmg = 0.0;
			breachA = 0.0;

			// If the attacker is being swarmed

			if (entity.getSwarmAttackerId() != Entity.NONE) {

				// Check for left arm punch damage to self

				odds = BrushOffAttackAction.toHit(game, entity.getId(),
						game.getEntity(entity.getSwarmAttackerId()),
						BrushOffAttackAction.LEFT);
				if (odds.getValue() != TargetRoll.IMPOSSIBLE) {

					lDmg = BrushOffAttackAction.getDamageFor(entity,
							BrushOffAttackAction.LEFT);
					lDmg *= 1.0 - Compute.oddsAbove(odds.getValue())
							/ MAX_VALUE;
					breach = punchThroughMod(entity, ToHitData.HIT_PUNCH,
							ToHitData.SIDE_FRONT, lDmg, lDmg);
					if (breach < MINIMAL_BREACH) {
						bestBrush = PhysicalOption.BRUSH_LEFT;
						breachA = breach;
						finalDmg = BrushOffAttackAction.getDamageFor(entity,
								BrushOffAttackAction.LEFT);
					}
				}

				// Check for right arm punch damage to self
				odds = BrushOffAttackAction.toHit(game, entity.getId(),
						game.getEntity(entity.getSwarmAttackerId()),
						BrushOffAttackAction.RIGHT);
				if (odds.getValue() != TargetRoll.IMPOSSIBLE) {

					// If chance of breaching armor is minimal set brush left

					rDmg = BrushOffAttackAction.getDamageFor(entity,
							BrushOffAttackAction.RIGHT);
					rDmg *= 1.0 - Compute.oddsAbove(odds.getValue())
							/ MAX_VALUE;
					breach = punchThroughMod(entity, ToHitData.HIT_PUNCH,
							ToHitData.SIDE_FRONT, rDmg, rDmg);
					if (breach < Math.min(breachA, MINIMAL_BREACH)) {
						bestBrush = PhysicalOption.BRUSH_RIGHT;
						breachA = breach;
						finalDmg = BrushOffAttackAction.getDamageFor(entity,
								BrushOffAttackAction.RIGHT);
					}
				}

				// If both arms are capable of punching, check double punch
				// damage

				if ((lDmg > 0) && (rDmg > 0)) {

					// If chance of breaching armor is minimal set double brush

					breach = punchThroughMod(entity, ToHitData.HIT_PUNCH,
							ToHitData.SIDE_FRONT, lDmg + rDmg,
							(lDmg + rDmg) / 2.0);
					if (breach < Math.min(breachA, MINIMAL_BREACH)) {
						bestBrush = PhysicalOption.BRUSH_BOTH;
						breachA = breach;
						finalDmg = BrushOffAttackAction.getDamageFor(entity,
								BrushOffAttackAction.LEFT);
						finalDmg += BrushOffAttackAction.getDamageFor(entity,
								BrushOffAttackAction.RIGHT);
					}
				}

				// Construct and return Physical option
				if (bestBrush != PhysicalOption.NONE) {
					return new PhysicalOption(entity, game.getEntity(entity
							.getSwarmAttackerId()), finalDmg, bestBrush, null);
				}
			}

			// If the attacker has attached iNarc pods, assign
			// a competing damage value for comparison with other
			// attacks

			if (entity.hasINarcPodsAttached()) {
				double testRanking;
				double podRanking;
				INarcPod testPod;
				INarcPod bestPod;
				podRanking = 0.0;
				Iterator<INarcPod> podList = entity.getINarcPodsAttached();
				bestPod = podList.next();
				for (podList = entity.getINarcPodsAttached(); podList.hasNext();) {
					testRanking = 1.0;
					testPod = podList.next();
					testRanking = assessTestRanking(entity, testRanking,
							testPod);
					// If this pod is best, retain it and its ranking
					if (testRanking > podRanking) {
						podRanking = testRanking;
						bestPod = testPod;
					}
				}
				if (bestPod != null) {
					// Check for left arm punch damage to self
					odds = BrushOffAttackAction.toHit(game, entity.getId(),
							bestPod, BrushOffAttackAction.LEFT);
					if (odds.getValue() != TargetRoll.IMPOSSIBLE) {

						lDmg = BrushOffAttackAction.getDamageFor(entity,
								BrushOffAttackAction.LEFT);
						lDmg *= 1.0 - Compute.oddsAbove(odds.getValue())
								/ MAX_VALUE;
						breach = punchThroughMod(entity, ToHitData.HIT_PUNCH,
								ToHitData.SIDE_FRONT, lDmg, lDmg);
						if (breach < MINIMAL_BREACH) {
							bestBrush = PhysicalOption.BRUSH_LEFT;
							breachA = breach;
							finalDmg = BrushOffAttackAction.getDamageFor(
									entity, BrushOffAttackAction.LEFT);
						}
					}

					// Check for right arm punch damage to self
					odds = BrushOffAttackAction.toHit(game, entity.getId(),
							bestPod, BrushOffAttackAction.RIGHT);
					if (odds.getValue() != TargetRoll.IMPOSSIBLE) {

						// If chance of breaching armor is minimal set brush
						// left

						rDmg = BrushOffAttackAction.getDamageFor(entity,
								BrushOffAttackAction.RIGHT);
						rDmg *= 1.0 - Compute.oddsAbove(odds.getValue())
								/ MAX_VALUE;
						breach = punchThroughMod(entity, ToHitData.HIT_PUNCH,
								ToHitData.SIDE_FRONT, rDmg, rDmg);
						if (breach < Math.min(breachA, MINIMAL_BREACH)) {
							bestBrush = PhysicalOption.BRUSH_RIGHT;
							breachA = breach;
							finalDmg = BrushOffAttackAction.getDamageFor(
									entity, BrushOffAttackAction.RIGHT);
						}
					}

					// If both arms are capable of punching, check double punch
					// damage

					if ((lDmg > 0) && (rDmg > 0)) {

						// If chance of breaching armor is minimal set double
						// brush

						breach = punchThroughMod(entity, ToHitData.HIT_PUNCH,
								ToHitData.SIDE_FRONT, lDmg + rDmg,
								(lDmg + rDmg) / 2.0);
						if (breach < Math.min(breachA, MINIMAL_BREACH)) {
							bestBrush = PhysicalOption.BRUSH_BOTH;
							finalDmg = BrushOffAttackAction.getDamageFor(
									entity, BrushOffAttackAction.LEFT);
							finalDmg += BrushOffAttackAction.getDamageFor(
									entity, BrushOffAttackAction.RIGHT);
						}
					}

					// Construct and return Physical option
					if (bestBrush != PhysicalOption.NONE) {
						return new PhysicalOption(entity, bestPod, finalDmg,
								bestBrush, null);
					}
				}
			}
		}

		best = findBestEntity(entity, game, best);
		if (best == null) {
			best = new PhysicalOption(entity);
		}
		return best;
	}

	/**
	 * @param entity
	 * @param game
	 * @param best
	 * @return
	 */
	private static PhysicalOption findBestEntity(final Entity entity,
			final IGame game, PhysicalOption best) {
		Entity target;
		for (Enumeration<Entity> e = game.getEntities(); e.hasMoreElements();) {
			target = e.nextElement();

			if ((target.equals(entity)) || (!target.isEnemyOf(entity))
					|| (target.getPosition() == null)
					|| (Compute.effectiveDistance(game, entity, target) > 1)) {
				continue;
			} else {
				best = getBestDamage(entity, game, best, target);
			}
		}
		return best;
	}

	/**
	 * @param entity
	 * @param game
	 * @param best
	 * @param target
	 * @return
	 */
	private static PhysicalOption getBestDamage(final Entity entity,
			final IGame game, PhysicalOption best, Entity target) {
		PhysicalOption one;
		one = getBestPhysicalAttack(entity, target, game);
		if ((one != null)
				&& ((best == null) || (one.expectedDmg > best.expectedDmg))) {
			best = one;
		}
		return best;
	}

	/**
	 * @param entity
	 * @param testRanking
	 * @param testPod
	 * @return
	 */
	private static double assessTestRanking(final Entity entity,
			double testRanking, INarcPod testPod) {
		// If pod is homing and attacker has no ECM
		if ((testPod.getType() == INarcPod.HOMING) && !entity.hasActiveECM()) {
			// Pod is +1
			testRanking += 1.0;
		}
		// If pod is ECM and attacker has C3 link
		if ((testPod.getType() == INarcPod.ECM)
				&& (entity.hasC3() || entity.hasC3i())) {
			// Pod is +2
			testRanking += 2.0;
		}
		// If pod is Nemesis
		if (testPod.getType() == INarcPod.NEMESIS) {
			// Pod is +variable, based on movement
			testRanking += (entity.getWalkMP() + entity.getJumpMP()) / 2.0;
		}
		return testRanking;
	}

	/**
	 * Gets the best physical attack.
	 * 
	 * @param from
	 *            the from
	 * @param to
	 *            the to
	 * @param game
	 *            the game
	 * @return the best physical attack
	 */
	static PhysicalOption getBestPhysicalAttack(Entity from, Entity to,
			IGame game) {
		double bestDmg = 0.0;
		double dmg;
		int damage;
		int targetArc;
		int locationTable;
		int bestType = PhysicalOption.NONE;
		Mounted bestClub = null;
		boolean targetConvInfantry = false;

		// Infantry and tanks can't conduct any of these attacks
		if ((from instanceof Infantry) || (from instanceof Tank)) {
			return null;
		}

		if ((to instanceof Infantry) && !(to instanceof BattleArmor)) {
			targetConvInfantry = true;
		}

		// Find arc the attack comes in
		targetArc = CEntity.getThreatHitArc(to.getPosition(), to.getFacing(),
				from.getPosition());

		// Check for punches
		// If the target is a Mech, must determine if punch lands on the punch,
		// kick, or full table
		if (to instanceof Mech) {
			locationTable = locationOfEntity(from, to);
		} else {
			locationTable = ToHitData.HIT_NORMAL;
		}

		ToHitData odds = PunchAttackAction.toHit(game, from.getId(), to,
				PunchAttackAction.LEFT);
		if (odds.getValue() != TargetRoll.IMPOSSIBLE) {
			damage = PunchAttackAction.getDamageFor(from,
					PunchAttackAction.LEFT, targetConvInfantry);
			bestDmg = Compute.oddsAbove(odds.getValue()) / MAX_VALUE * damage;
			// Adjust damage for targets armor
			bestType = PhysicalOption.PUNCH_LEFT;
			bestDmg *= punchThroughMod(to, locationTable, targetArc, bestDmg,
					bestDmg);
		}

		odds = PunchAttackAction.toHit(game, from.getId(), to,
				PunchAttackAction.RIGHT);
		if (odds.getValue() != TargetRoll.IMPOSSIBLE) {
			damage = PunchAttackAction.getDamageFor(from,
					PunchAttackAction.RIGHT, targetConvInfantry);
			dmg = Compute.oddsAbove(odds.getValue()) / MAX_VALUE * damage;
			// Adjust damage for targets armor
			dmg *= punchThroughMod(to, locationTable, targetArc, dmg, dmg);
			if (dmg > bestDmg) {
				bestType = PhysicalOption.PUNCH_RIGHT;
				bestDmg = dmg;
			}
		}

		// Check for a double punch
		odds = PunchAttackAction.toHit(game, from.getId(), to,
				PunchAttackAction.LEFT);
		ToHitData oddsA = PunchAttackAction.toHit(game, from.getId(), to,
				PunchAttackAction.RIGHT);
		if ((odds.getValue() != TargetRoll.IMPOSSIBLE)
				&& (oddsA.getValue() != TargetRoll.IMPOSSIBLE)) {
			damage = PunchAttackAction.getDamageFor(from,
					PunchAttackAction.LEFT, targetConvInfantry);
			dmg = Compute.oddsAbove(odds.getValue()) / MAX_VALUE * damage;
			double dmgA = Compute.oddsAbove(oddsA.getValue()) / MAX_VALUE
					* damage;
			dmg += dmgA;
			dmg *= punchThroughMod(to, locationTable, targetArc, dmg, dmg / 2.0);
			if (dmg > bestDmg) {
				bestType = PhysicalOption.PUNCH_BOTH;
				bestDmg = dmg;
			}
		}

		locationTable = setLocationOfMechKick(from, to);

		dmg = getExpectedKickDamage(from, to, game, locationTable, targetArc,
				KickAttackAction.LEFT);
		if (dmg > bestDmg) {
			bestType = PhysicalOption.KICK_LEFT;
			bestDmg = dmg;
		}

		dmg = getExpectedKickDamage(from, to, game, locationTable, targetArc,
				KickAttackAction.RIGHT);
		if (dmg > bestDmg) {
			bestType = PhysicalOption.KICK_RIGHT;
			bestDmg = dmg;
		}

		// Check for mounted club-type weapon or carried improvised club
		for (Mounted club : from.getClubs()) {
			// If the target is a Mech, must determine if it hits full body,
			// punch, or kick table
			locationTable = setLocationOfHitOnMech(from, to);
			odds = ClubAttackAction.toHit(game, from.getId(), to, club,
					ToHitData.HIT_NORMAL);
			if (odds.getValue() != TargetRoll.IMPOSSIBLE) {
				damage = ClubAttackAction.getDamageFor(from, club,
						targetConvInfantry);
				dmg = Compute.oddsAbove(odds.getValue()) / MAX_VALUE * damage;
				// Adjust damage for targets armor
				dmg *= punchThroughMod(to, locationTable, targetArc, dmg, dmg);
				// Some types of clubs, such as the mace, require a piloting
				// check on a missed attack
				// Calculate self damage in the same manner as a missed kick
				if (dmg > bestDmg) {
					bestType = PhysicalOption.USE_CLUB;
					bestDmg = dmg;
					bestClub = club;
				}
			}
		}
		// Check for a push attack
		odds = PushAttackAction.toHit(game, from.getId(), to);
		if (odds.getValue() != TargetRoll.IMPOSSIBLE) {
			int elevDiff;
			double breach;
			boolean waterLanding = false;
			dmg = 0.0;
			int dispDir = from.getPosition().direction(to.getPosition());
			Coords dispC = to.getPosition().translated(dispDir);
			// If the displacement hex is a valid one
			if (Compute.isValidDisplacement(game, to.getId(), to.getPosition(),
					dispC)) {
				// If the displacement hex is not on the map, credit damage
				// against full target armor
				if (!game.getBoard().contains(dispC)) {
					dmg = to.getTotalArmor()
							* Compute.oddsAbove(odds.getValue()) / MAX_VALUE;
				}
				if (game.getBoard().contains(dispC)) {
					// Find the elevation difference
					elevDiff = game.getBoard().getHex(to.getPosition())
							.getElevation();
					elevDiff -= game.getBoard().getHex(dispC).getElevation();
					if (elevDiff < 0) {
						elevDiff = 0;
					}
					// Set a flag if the displacement hex has water
					if (game.getBoard().getHex(dispC)
							.containsTerrain(Terrains.WATER)) {
						waterLanding = true;
					}
					// Get the base damage from target falling, multiplied by
					// the elevation difference
					dmg = calculateFallingDamage(
							Compute.oddsAbove(odds.getValue()) / MAX_VALUE, to)
							* (1.0 + elevDiff);
					// Calculate breach factor of falling damage
					dmg = calculateBreachFactor(to, dmg, waterLanding);
				}
			}
			// If the displacement hex is not valid
			if (!Compute.isValidDisplacement(game, to.getId(),
					to.getPosition(), dispC)) {
				// Set a flag if the displacement hex has water
				if (game.getBoard().getHex(to.getPosition())
						.containsTerrain(Terrains.WATER)) {
					waterLanding = true;
				}
				// Get falling in place
				dmg = calculateFallingDamage(Compute.oddsAbove(odds.getValue())
						/ MAX_VALUE, to);
				// Calculate breach factor of falling damage
				dmg = calculateBreachFactor(to, dmg, waterLanding);
			}
			// If damage is better than best damage
			if (dmg > bestDmg) {
				bestType = PhysicalOption.PUSH_ATTACK;
				bestDmg = dmg;
			}
		}

		// Conventional infantry in the open suffer double damage.
		if ((to instanceof Infantry) && !(to instanceof BattleArmor)) {
			IHex eHex = game.getBoard().getHex(to.getPosition());
			if (!eHex.containsTerrain(Terrains.WOODS)
					&& !eHex.containsTerrain(Terrains.BUILDING)) {
				bestDmg *= 2.0;
			}
		}

		if (bestDmg > 0) {
			return new PhysicalOption(from, to, bestDmg, bestType, bestClub);
		}
		return null;
	}

	/**
	 * @param from
	 * @param to
	 * @return
	 */
	private static int setLocationOfHitOnMech(Entity from, Entity to) {
		int locationTable;
		if (to instanceof Mech) {
			locationTable = ToHitData.HIT_NORMAL;
			if ((to.getElevation() == from.getElevation() - 1)
					&& !to.isProne()) {
				locationTable = ToHitData.HIT_PUNCH;
			}
			if ((to.getElevation() == from.getElevation() + 1)
					&& !to.isProne()) {
				locationTable = ToHitData.HIT_KICK;
			}
		} else {
			locationTable = ToHitData.HIT_NORMAL;
		}
		return locationTable;
	}

	/**
	 * @param from
	 * @param to
	 * @return
	 */
	private static int setLocationOfMechKick(Entity from, Entity to) {
		int locationTable;
		// Check for a kick
		// If the target is a Mech, must determine if it lands on the kick or
		// punch table
		if (to instanceof Mech) {
			locationTable = ToHitData.HIT_KICK;
			if (!to.isProne()) {
				if (to.getElevation() == from.getElevation() - 1) {
					locationTable = ToHitData.HIT_PUNCH;
				}
			} else {
				locationTable = ToHitData.HIT_NORMAL;
			}
		} else {
			locationTable = ToHitData.HIT_NORMAL;
		}
		return locationTable;
	}

	/**
	 * @param from
	 * @param to
	 * @return
	 */
	private static int locationOfEntity(Entity from, Entity to) {
		int locationTable;
		if (!to.isProne()) {
			locationTable = ToHitData.HIT_PUNCH;
			if (to.getElevation() == from.getElevation() + 1) {
				locationTable = ToHitData.HIT_KICK;
			}
		} else {
			locationTable = ToHitData.HIT_NORMAL;
		}
		return locationTable;
	}

	/**
	 * @param to
	 * @param dmg
	 * @param waterLanding
	 * @return
	 */
	private static double calculateBreachFactor(Entity to, double dmg,
			boolean waterLanding) {
		double breach;
		// Calculate breach factor of falling damage
		breach = punchThroughMod(to, ToHitData.HIT_NORMAL,
				ToHitData.SIDE_FRONT, dmg, Math.min(dmg, 5.0));
		// If breach factor is > 1 and displacement hex has water
		if ((breach > 1) && waterLanding) {
			breach *= 2.0;
		}
		// Modify damage to reflect how bad it is for target to be
		// prone
		if (to.getWalkMP() > 0) {
			dmg = dmg * Math.sqrt(1.0 / to.getWalkMP() + to.getJumpMP());
		} else {
			dmg *= Math.max(1.0, Math.sqrt(to.getJumpMP()));
		}
		// Modify damage by breach factor
		dmg *= breach;
		return dmg;
	}

	/**
	 * Calculates the Falling damage after a successful To-Hit.
	 * 
	 * @param odds
	 *            the odds
	 * @param ent
	 *            The entity that is falling
	 * @return the double
	 */
	private static double calculateFallingDamage(double odds, Entity ent) {
		double dmg = odds;
		dmg *= 1.0 - Compute.oddsAbove(ent.getBasePilotingRoll().getValue())
				/ MAX_VALUE;
		dmg *= ent.getWeight() * 0.1;
		return dmg;
	}

	/**
	 * Gets the expected kick damage.
	 * 
	 * @param from
	 *            the from
	 * @param to
	 *            the to
	 * @param game
	 *            the game
	 * @param locTable
	 *            the loc table
	 * @param arc
	 *            the arc
	 * @param action
	 *            the action
	 * @return the expected kick damage
	 */
	private static double getExpectedKickDamage(Entity from, Entity to,
			IGame game, int locTable, int arc, int action) {
		double selfDamage;
		double dmg;
		double collDamage = 0.0;
		int damage;
		boolean targetConvInfantry = false;
		ToHitData odds = KickAttackAction.toHit(game, from.getId(), to, action);
		if (odds.getValue() == TargetRoll.IMPOSSIBLE) {
			return 0.0;
		}

		if ((to instanceof Infantry) && !(to instanceof BattleArmor)) {
			targetConvInfantry = true;
		}

		// Calculate collateral damage, due to possible target fall
		if (to instanceof Mech) {
			collDamage = calculateFallingDamage(
					Compute.oddsAbove(odds.getValue()) / MAX_VALUE, to);
		}

		damage = KickAttackAction
				.getDamageFor(from, action, targetConvInfantry);
		dmg = Compute.oddsAbove(odds.getValue()) / MAX_VALUE * damage;
		// Adjust damage for targets armor
		dmg *= punchThroughMod(to, locTable, arc, dmg, dmg);
		// Calculate self damage, due to possible fall from missing a kick
		selfDamage = calculateFallingDamage(
				1.0 - Compute.oddsAbove(odds.getValue()) / MAX_VALUE, from);
		if (from.getWalkMP() > 0) {
			selfDamage = selfDamage
					* Math.sqrt(1.0 / from.getWalkMP() + from.getJumpMP());
		} else {
			selfDamage = selfDamage * Math.sqrt(from.getJumpMP());
		}
		// Add together damage values for comparison
		dmg = dmg + collDamage - selfDamage;
		return dmg;
	}

	/**
	 * This checks to see if the damage will punch through armor anywhere in the
	 * attacked arc. damage argument is divided into hits, using the group
	 * argument (ie, group = 5.0 for LRM). Each hit of group damage is checked
	 * against each location; if it penetrates increase the multiplier to
	 * reflect potential for additional damage Multiple passes are made with
	 * each hit being multiples of group damage to reflect shot grouping; as
	 * each pass is made the increase to the multiplier is lowered due to the
	 * lower chance of hitting the same location
	 * 
	 * @param target
	 *            the target
	 * @param hitTable
	 *            the hit table
	 * @param hitSide
	 *            the hit side
	 * @param damage
	 *            the damage
	 * @param group
	 *            the group
	 * @return the double
	 */
	private static double punchThroughMod(Entity target, int hitTable,
			int hitSide, double damage, double group) {

		int[] armorValues = new int[8];
		int maxIndex = 1;
		armorValues[0] = 0;

		// Set the final multiplier as 1.0 (no chance of penetrating armor)
		double finalMultiplier = 1.0;

		// Set the base multiplier as 0.5 (good bonus for penetrating with a
		// single hit)
		double baseMultiplier = 0.5;

		if ((damage <= 0.0) || (group <= 0.0)) {
			return finalMultiplier;
		}

		// If the target is a Mech
		if (target instanceof Mech) {
			// Create vector of body locations with targets current armor values
			// Use hit table and direction to determine locations that are hit
			if (hitTable == ToHitData.HIT_NORMAL) {
				maxIndex = 7;
				armorValues[0] = target.getArmor(Mech.LOC_HEAD, false);
				if (hitSide != ToHitData.SIDE_FRONT) {
					armorValues[1] = target.getArmor(Mech.LOC_CT, true);
				} else {
					armorValues[1] = target.getArmor(Mech.LOC_CT, false);
				}
				if (hitSide != ToHitData.SIDE_FRONT) {
					armorValues[2] = target.getArmor(Mech.LOC_RT, true);
				} else {
					armorValues[2] = target.getArmor(Mech.LOC_RT, false);
				}
				if (hitSide != ToHitData.SIDE_FRONT) {
					armorValues[3] = target.getArmor(Mech.LOC_LT, true);
				} else {
					armorValues[3] = target.getArmor(Mech.LOC_LT, false);
				}
				armorValues[4] = target.getArmor(Mech.LOC_RARM, false);
				armorValues[5] = target.getArmor(Mech.LOC_LARM, false);
				armorValues[6] = target.getArmor(Mech.LOC_RLEG, false);
				armorValues[7] = target.getArmor(Mech.LOC_RLEG, false);
			}
			if (hitTable == ToHitData.HIT_PUNCH) {
				armorValues[0] = target.getArmor(Mech.LOC_HEAD, false);
				if (hitSide == ToHitData.SIDE_RIGHT) {
					maxIndex = 3;
					armorValues[1] = target.getArmor(Mech.LOC_CT, false);
					armorValues[2] = target.getArmor(Mech.LOC_RT, false);
					armorValues[3] = target.getArmor(Mech.LOC_RARM, false);
				}
				if (hitSide == ToHitData.SIDE_LEFT) {
					maxIndex = 3;
					armorValues[1] = target.getArmor(Mech.LOC_CT, false);
					armorValues[2] = target.getArmor(Mech.LOC_LT, false);
					armorValues[3] = target.getArmor(Mech.LOC_LARM, false);
				}
				if (hitSide == ToHitData.SIDE_FRONT) {
					maxIndex = 5;
					armorValues[1] = target.getArmor(Mech.LOC_CT, false);
					armorValues[2] = target.getArmor(Mech.LOC_RT, false);
					armorValues[3] = target.getArmor(Mech.LOC_LT, false);
					armorValues[4] = target.getArmor(Mech.LOC_RARM, false);
					armorValues[5] = target.getArmor(Mech.LOC_LARM, false);
				}
				if (hitSide == ToHitData.SIDE_REAR) {
					maxIndex = 5;
					armorValues[1] = target.getArmor(Mech.LOC_CT, true);
					armorValues[2] = target.getArmor(Mech.LOC_RT, true);
					armorValues[3] = target.getArmor(Mech.LOC_LT, true);
					armorValues[4] = target.getArmor(Mech.LOC_RARM, false);
					armorValues[5] = target.getArmor(Mech.LOC_LARM, false);
				}
			}
			if (hitTable == ToHitData.HIT_KICK) {
				maxIndex = -1;
				if ((hitSide == ToHitData.SIDE_FRONT)
						|| (hitSide == ToHitData.SIDE_REAR)
						|| (hitSide == ToHitData.SIDE_RIGHT)) {
					maxIndex++;
					armorValues[maxIndex] = target.getArmor(Mech.LOC_RLEG,
							false);
				}
				if ((hitSide == ToHitData.SIDE_FRONT)
						|| (hitSide == ToHitData.SIDE_REAR)
						|| (hitSide == ToHitData.SIDE_LEFT)) {
					maxIndex++;
					armorValues[maxIndex] = target.getArmor(Mech.LOC_LLEG,
							false);
				}
			}
		}
		// If the target is a ProtoMech
		if (target instanceof Protomech) {
			maxIndex = 6;
			// Create vector of body locations with targets current armor values
			// Create two high-armor dummy locations to represent the 'near
			// miss' hit locations
			armorValues[0] = target.getArmor(Protomech.LOC_TORSO, false);
			armorValues[1] = target.getArmor(Protomech.LOC_LEG, false);
			armorValues[2] = target.getArmor(Protomech.LOC_RARM, false);
			armorValues[3] = target.getArmor(Protomech.LOC_LARM, false);
			armorValues[4] = target.getArmor(Protomech.LOC_HEAD, false);
			armorValues[5] = 100;
			armorValues[6] = 100;
			if (((Protomech) target).hasMainGun()) {
				maxIndex++;
				armorValues[maxIndex] = target.getArmor(Protomech.LOC_MAINGUN,
						false);
			}
		}
		// If the target is a vehicle
		if (target instanceof Tank) {
			// Create vector of armor locations
			maxIndex = 0;
			switch (hitSide) {
			case ToHitData.SIDE_FRONT:
				armorValues[0] = target.getArmor(Tank.LOC_FRONT);
				break;
			case ToHitData.SIDE_RIGHT:
				armorValues[0] = target.getArmor(Tank.LOC_RIGHT);
				break;
			case ToHitData.SIDE_LEFT:
				armorValues[0] = target.getArmor(Tank.LOC_LEFT);
				break;
			case ToHitData.SIDE_REAR:
				armorValues[0] = target.getArmor(Tank.LOC_REAR);
				break;
			}
			if (!((Tank) target).hasNoTurret()) {
				maxIndex++;
				armorValues[maxIndex] = target.getArmor(Tank.LOC_TURRET);
			}
		}
		// If the target is Battle Armor
		if (target instanceof BattleArmor) {
			// Create vector of armor of surviving troopers
			maxIndex = -1;
			int strength = ((BattleArmor) target).getShootingStrength();
			for (int i = 1; i < strength; i++) {
				if (target.getArmor(i) >= 0) {
					maxIndex++;
					armorValues[maxIndex] = target.getArmor(i);
				}
			}
		}
		// If the target is conventional infantry
		if ((target instanceof Infantry) && !(target instanceof BattleArmor)) {
			// Create a single element vector with total number of troopers
			maxIndex = 0;
			armorValues[0] = ((Infantry) target).getShootingStrength();
		}

		finalMultiplier = applyTotalDamage(damage, group, armorValues,
				maxIndex, finalMultiplier, baseMultiplier);

		// Return final multiplier

		return finalMultiplier;

	}

	/**
	 * @param damage
	 * @param group
	 * @param armorValues
	 * @param maxIndex
	 * @param finalMultiplier
	 * @param baseMultiplier
	 * @return
	 */
	private static double applyTotalDamage(double damage, double group,
			int[] armorValues, int maxIndex, double finalMultiplier,
			double baseMultiplier) {
		double hitTotal = 0;
		// While hit damage is less than total damage applied, increment by
		// group value
		while (hitTotal <= damage) {
			hitTotal += group;
			finalMultiplier = increaseFinalDamageMultiplier(armorValues,
					maxIndex, finalMultiplier, baseMultiplier, hitTotal);
			baseMultiplier /= 2.0;
		}
		return finalMultiplier;
	}

	/**
	 * @param armorValues
	 * @param maxIndex
	 * @param finalMultiplier
	 * @param baseMultiplier
	 * @param hitTotal
	 * @return
	 */
	private static double increaseFinalDamageMultiplier(int[] armorValues,
			int maxIndex, double finalMultiplier, double baseMultiplier,
			double hitTotal) {
		for (int i = 0; i <= maxIndex; i++) {
			// If hit damage can penetrate location
			if (hitTotal > armorValues[i]) {
				finalMultiplier += baseMultiplier;
			}
		}
		return finalMultiplier;
	}
}
